Repository navigation
chore: regenerate the artifacts at the top of the access level and edition tier stack - #1571
Merged
Merged
Conversation
…ition tier stack The four layers below regenerate nothing, under the freshness deferral, and they changed the surface every generated artifact describes: the access level parameters publish the levels each route accepts as integer enums, the group access request approval reads the group and the requester for a Minimal access approval, and the group, member, LDAP link, group variable, issue, merge request, notification and project parameters are tier tagged or described by tier. The tool snapshots of the individual surface and of the opaque, compact and full meta schemas follow, and with them the per-domain tool reference in English and Spanish (access, admin, ci-variable, group, issue, member-role, merge-request, project and user), the four llms files, the token footprint with the README block it writes, and the testing reference, refreshed with -skip-coverage. The action request record and the fine-grained table gain the two optional reads of access.approve_group, which the permissions reference prints. The request inventory was recorded again on the whole unit suite and merged: it gains those two reads and the new_epic flag a group notification settings update sends. The site stats, the brand assets, the manifests, the model evaluation pages, the charts and the e2e coverage page regenerate unchanged. R-PATH declares the group read as one the package makes for its own use, beside the LDAP link listing's existence probe: it reads parent_id alone to refuse the approval on a subgroup. The declaration lands here because only this commit's inventory holds the read, and a declaration that answers nothing fails the audit. Refs #1280 Refs #1240 Signed-off-by: José M. Requena Plens <mail@jmrp.io>
|
Sorry @jmrplens, your pull request is larger than the review limit of 150,000 diff characters |
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 59 minutes. View limit details
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
This was referenced Oct 28, 2026
Merged
jmrplens
added a commit
that referenced
this pull request
Oct 28, 2026
…ists, and publish the eight sort orders they document (#1573) Refs #1165 This is the bottom layer of the first #1165 stack. It classifies every action that publishes `page_token` by what GitLab 19.5.0 does with a keyset request on its route, and it changes the two service account token lists, the two routes where GitLab answers every keyset request with 405, which the issue's 2026-10-10 decision covers. On those two lists it removes the keyset input and an `order_by` GitLab never reads, and it publishes the eight sort orders the routes document in place of the `asc` and `desc` they could not use. ## The classification I read every route at `v19.5.0-ee` of GitLab's own source. Each action's routes come from `docs/development/action-requests.json`, each route was matched to its Grape `get`, and the endpoint body or the helper it calls was read for its paginator: offset `paginate` never reads `pagination`; `paginate_with_strategies` takes keyset with `CursorPager` (cursor `cursor`) when the relation's class has the requested ordering among its `supported_keyset_orderings`, falls back to `Gitlab::Pagination::Keyset` (Project ordered by `id` alone, `id_after`/`id_before`), and otherwise answers `405 Keyset pagination is not yet available for this type of request`; `GitalyKeysetPager` serves branches, tags, the tree and, behind a flag, commits by `page_token`. `page_token` is published by 202 individual tools, which carry 204 actions (`gitlab_commit_list` serves `repository.commit_list` and `repository.file_history`, and `gitlab_issue_list_group` serves `group.issues` and `issue.list_group`). | Class | Actions | What GitLab does with `pagination=keyset` | | --- | --- | --- | | follows keyset | 13 | pages by keyset for every ordering the action can send | | 405 by ordering | 5 | pages by keyset for some orderings and answers 405 for the rest | | 405 always | 2 | answers 405 whatever the request | | keyset not followed | 1 | answers 405 with the default ordering and, ordered by `id`, serves a keyset page but never applies the position its own next link names | | ignored | 183 | never reads it: 177 routes page by offset `paginate`, 6 are unpaginated or a single object | The 13 that follow keyset are the three audit event lists (`cursor`), `branch.list`, `tag.list` and `repository.tree` (`page_token`), `issue.list` (`cursor`), `package.registry_tag_list` (`last`), `runner.jobs`, `user.list`, `user.list_service_accounts` and the group and project service account lists (`cursor`). Two of them carry a condition: `branch.list` with `search` or `regex` uses the legacy finder, where keyset needs `branch_list_keyset_pagination` (off by default) and is ignored, and on `issue.list` an `order_by` outside the Issue keyset orderings makes GitLab delete `pagination` and answer offset. The 5 refused by ordering are `project.list`, `project.list_forks`, `project.list_user_projects` and `project.list_user_starred`, which follow keyset only with `order_by=id` (the default is `created_at`, so a keyset request that leaves `order_by` out is refused), and `job.list_project`, which follows keyset only ordered by `id`, its default. The 2 refused always are `group.service_account_pat_list` and `project.service_account_pat_list`: `paginate_with_strategies` over a PersonalAccessToken relation, a class with no keyset ordering that is not a Project. The one not followed is `project.list_user_contributed`. Its route reaches the same paginator as the other project lists, so a keyset request with the default `created_at` ordering is answered with 405 as theirs is. With `order_by=id` it is served a keyset page, but the route builds `ContributedProjectsFinder` with no params (`lib/api/projects.rb:396`), so the `id_after` or `id_before` its next link names is never applied and every keyset page is the first one again. That is the upstream bug register's row 112, and `project.list_user_contributed` already refuses `pagination` and `page_token` before any request, so no caller reaches either answer. By the issue's decision its keyset input should not be offered either. It shares `userProjectFilterInput` with the two user project lists that do follow keyset, so removing it means splitting that input in `internal/tools/projects/projects.go`. I left it to the layer of the second #1165 stack that removes the keyset input from every list that cannot use it: that layer changes the file anyway, and it holds this action to the same rule as the lists GitLab pages by offset. The table agrees with the 20 `toolutil.KeysetContinuation` call sites. The 18 actions that follow keyset or are refused by ordering pass it, in 17 sites: one, in `listUserScopedProjects`, serves both `project.list_user_projects` and `project.list_user_starred`. `project.list_user_contributed` calls the same helper but refuses keyset before reaching it, and the two refused always pass none. The other three sites sit on routes classed ignored. Two of those are dead: `group.ssh_cert_list` and `project.upload_list` page by offset `paginate`. The third is not: `repository.commit_list` and `repository.file_history` follow keyset by `page_token` when `commits_keyset_pagination` is on, a GitLab.com rollout flag that is off by default on 19.5.0, so that site has to stay when the offset lists lose their keyset input. Three routes behave differently from what I expected before reading them. Every `order_by` that `/users` declares is a User keyset ordering, so the users list refuses none. Project issues fall back to offset rather than answering 405. The audit event lists send no ordering, and their routes declare none. So the only ordering refusals a caller can reach are the four project lists above and project jobs. <details> <summary>The 21 actions GitLab can page by keyset, or refuses it on</summary> | Action | Route | GitLab `v19.5.0-ee` | Class | Cursor | | --- | --- | --- | --- | --- | | `audit_event.list_group` | `GET /groups/:id/audit_events` | `ee/lib/ee/api/groups.rb:257` | follows keyset | `cursor` | | `audit_event.list_instance` | `GET /audit_events` | `ee/lib/api/audit_events.rb:55` | follows keyset | `cursor` | | `audit_event.list_project` | `GET /projects/:id/audit_events` | `ee/lib/ee/api/projects.rb:63` | follows keyset | `cursor` | | `branch.list` | `GET /projects/:id/repository/branches` | `lib/api/branches.rb:83` | follows keyset | `page_token` | | `group.service_account_list` | `GET /groups/:id/service_accounts` | `lib/api/group_service_accounts.rb:105` | follows keyset | `cursor` | | `issue.list` | `GET /projects/:id/issues` | `lib/api/issues.rb:272` | follows keyset | `cursor` | | `package.registry_tag_list` | `GET /projects/:id/registry/repositories/:repository_id/tags` | `lib/api/project_container_repositories.rb:111` | follows keyset | `last` | | `project.service_account_list` | `GET /projects/:id/service_accounts` | `lib/api/project_service_accounts.rb:97` | follows keyset | `cursor` | | `repository.tree` | `GET /projects/:id/repository/tree` | `lib/api/repositories.rb:170` | follows keyset | `page_token` | | `runner.jobs` | `GET /runners/:id/jobs` | `lib/api/ci/runners.rb:352` | follows keyset | `cursor` | | `tag.list` | `GET /projects/:id/repository/tags` | `lib/api/tags.rb:62` | follows keyset | `page_token` | | `user.list` | `GET /users` | `lib/api/users.rb:267` | follows keyset | `cursor` | | `user.list_service_accounts` | `GET /service_accounts` | `lib/api/service_accounts.rb:77` | follows keyset | `cursor` | | `job.list_project` | `GET /projects/:id/jobs` | `lib/api/ci/jobs.rb:72` | 405 by ordering | `cursor` | | `project.list` | `GET /projects` | `lib/api/projects.rb:248` | 405 by ordering | `id_after` / `id_before` | | `project.list_forks` | `GET /projects/:id/forks` | `lib/api/projects.rb:248` | 405 by ordering | `id_after` / `id_before` | | `project.list_user_projects` | `GET /users/:user_id/projects` | `lib/api/projects.rb:248` | 405 by ordering | `id_after` / `id_before` | | `project.list_user_starred` | `GET /users/:user_id/starred_projects` | `lib/api/projects.rb:248` | 405 by ordering | `id_after` / `id_before` | | `group.service_account_pat_list` | `GET /groups/:id/service_accounts/:user_id/personal_access_tokens` | `lib/api/group_service_accounts.rb:240` | 405 always | none | | `project.service_account_pat_list` | `GET /projects/:id/service_accounts/:user_id/personal_access_tokens` | `lib/api/project_service_accounts.rb:217` | 405 always | none | | `project.list_user_contributed` | `GET /users/:user_id/contributed_projects` | `lib/api/projects.rb:396` | keyset not followed | `id_after` / `id_before`, never applied | </details> The other 183 actions, the ones GitLab pages by offset or not at all, are listed with their routes in the pull request that removes their keyset input, since that is where the list is acted on. `mr_review.changes_get` is classified like the rest (ignored: `lib/api/merge_requests.rb:719` pages the diffs by offset) and its code is left alone here: #1517 changes that handler and pages its diffs by offset, and the unused `order_by`, `sort`, `pagination` and `page_token` on its input are #1530. ## What changed `ListPATInput` in `internal/tools/groupserviceaccounts` and `internal/tools/projectserviceaccounts` now embeds `PaginationInput` alone, has no `order_by`, and `ListPATs` passes a zero `KeysetPaginationInput` to `ApplyListOptions`, as the offset-only lists do. On these routes `pagination=keyset` is answered with 405, `page_token` without it is ignored by the offset paginator, which reads `page` and `per_page` alone, and `order_by` is declared by neither route, so the token finder, which is handed the declared params alone and orders by `sort`, never saw it. A caller that sends any of the three is now told the field is unknown before any request is made, and the published schema of both actions offers `page` and `per_page` and none of them. R-INPUT now sees three fields of client-go's `ListOptions` missing from each input, so I answered them in `acceptedMissingInputs` with one reason naming the two routes, the paginator, the 405 and the undeclared `order_by`. Both lists also published `sort` with the `asc` and `desc` enum the catalog gives every sort parameter. These routes take a named order, and document eight: `created_asc`, `created_desc`, `expires_asc`, `expires_desc`, `last_used_asc`, `last_used_desc`, `name_asc` and `name_desc` (`access_token_params` in `lib/api/helpers/personal_access_tokens_helpers.rb`). `PersonalAccessTokensFinder` sorts by a key of `PersonalAccessToken.simple_sorts` and leaves the tokens unsorted for any other value, and `asc` and `desc` are not keys, so the schema refused every order GitLab applies and admitted only two values it ignores. The `pat_list` action of each package now overrides the enum with the eight documented orders, as the three access token lists already do, and the description names them. That is its own commit, since it is not about keyset. `simple_sorts` holds nine more keys, which `PersonalAccessToken` inherits from `Sortable` and the routes do not document: `id_asc`, `id_desc`, `created_at_asc`, `created_at_desc`, `created_date`, `updated_asc`, `updated_desc`, `updated_at_asc` and `updated_at_desc`. GitLab applies them, and the example request on both documentation pages sends `sort=id_desc`, a value outside the list printed above it. I kept the enum to the documented eight, as the access token lists do, said so in the comment beside it, and recorded the mismatch in the upstream bug register as row 143, a documentation fix to propose upstream. If we would rather publish what GitLab applies, the enum can take all seventeen; R-ENUM cannot tell the two apart, since client-go models the field as a `*string`. ## How I tested it I wrote and measured this layer on `e3e09d290` and then rebased the stack twice: onto `main` at `6c9066aef`, after the OpenSSF Best Practices stack, and onto `main` at `56758fb71`, after the access level and edition tier stack (#1567 to #1571). The rebases changed one thing in this layer, the number of its row in the upstream bug register: `main` had taken row 140 for gobco, and then row 141 for the custom role and broadcast message pages and row 142 for the API pages' tier markers, so the token list row is 143, in the register and in the comment beside each enum. In the Go files this layer touches, `main` changed the copyright and SPDX header at their top and, in `cmd/audit_1to1/internal/structs/analyze.go`, added three R-INPUT answers of its own (the notification `new_epic` and the two project checksum filters) away from the six this layer adds, which merged without a conflict. What I ran again after the rebases is at the end of this section. Tests first. On `e3e09d290`, `TestListPATs_KeysetArgument_IsRefusedBeforeAnyRequest` and `TestListPATInput_Schema_OffersOffsetPaginationOnly`, in each package, failed for every one of the three fields: the route sent the request to a mock that forbids any (`mock API was called 1 time(s); the test forbids any request`) and the schema offered the field (`schema offers order_by = true, want false`). `TestActionSpecs_TokenSortEnum_NamesTheEightDocumentedOrders`, in each package, failed with `pat_list sort enum = [asc desc], want [created_asc created_desc expires_asc expires_desc last_used_asc last_used_desc name_asc name_desc]`. All of them pass on this head. The existing keyset case of the group token list test now checks offset pagination instead, the project one no longer sends the keyset fields or `order_by`, and the group one sends `created_desc` rather than `desc`. On the layer's head before the rebases, with `GITLAB_MCP_TEST_SNAPSHOT_PARITY=deferred` since it is below the stack's top: - `go test ./... -count=1`: all 294 packages pass. - `groupserviceaccounts`, `projectserviceaccounts` and `cmd/audit_1to1/internal/structs`: 100% statement coverage; condition coverage 158/158, 210/210 and 426/426; mutants Killed 75, 92 and 198, with Lived 0, Not covered 0 and Timed out 0 in all three. - `go run ./cmd/audit_1to1/ -gaps-only`: the backlog is byte for byte the one on `e3e09d290` (20 missing inputs, 0 stale declarations), so the six new answers are each used and no finding is new. `make audit-1to1` passes. - `golangci-lint run --build-tags e2e` and `golangci-lint fmt --diff` on the three packages, `go test -tags e2e -c ./test/e2e/gitlab/...`, and `make check-e2e-static`, `check-test-file-names`, `check-test-subtests`, `check-test-goroutines`, `check-action-ids`, `check-gateway-chars`, `check-dead-consts`, `check-meta-descriptions`, `check-surface-quality`, `check-tenancy`, `check-doc-tool-names`, `check-md-tables`, `check-md-escaping`, `check-sdk-context` and `audit-catalog-first`: all pass. - `docs/development/upstream-bugs.md`, for the token list row: markdownlint and `node scripts/check-doc-links.mjs` pass. With the comparison checked, `TestToolSnapshots_Individual` and the compact and full cases of `TestToolSnapshots_MetaParamSchemaModes` report exactly the two token list tools and the `gitlab_group` and `gitlab_project` schemas that carry them, which is the regeneration the top does. The curated `tools_meta.json` does not change. No catalog action is added or removed, and the e2e scenarios of both token lists send neither keyset field, `order_by` nor `sort`, so no scenario changes and no Docker run was made for this layer. After the second rebase, on this head: `GITLAB_MCP_TEST_SNAPSHOT_PARITY=deferred go test ./... -count=1` passes (301 packages), `golangci-lint run --build-tags e2e` and `golangci-lint fmt --diff` pass on the three packages, the e2e packages compile, and every gate of the list above passes again, `make audit-1to1` included. `go run ./cmd/audit_1to1/ -gaps-only` writes a backlog byte for byte the one `main` writes at `56758fb71`, which is the one it wrote at `6c9066aef` (20 missing inputs, 0 stale declarations). `make check-file-headers` and `make check-dco` against `main` pass. Markdownlint and `node scripts/check-doc-links.mjs` pass at the top of the stack, which holds the register as it is here. I did not measure coverage, conditions or mutants again: the rebases changed no statement this layer wrote in the three packages, only the comment that names the register row and the header lines at their top. ## What is left for the top of the stack This layer regenerates no committed artifact, under the freshness deferral. The top regenerates the tool snapshots, the per-domain tool reference in both languages, the token footprint, the llms files, the request inventory and the testing reference once. #1517 regenerates some of the same files (the tool snapshots, the request inventory, the token footprint, the testing reference and the llms files), so whichever of the two lands second regenerates them again. The two share no hand-written file. --------- Signed-off-by: José M. Requena Plens <mail@jmrp.io>
jmrplens
added a commit
that referenced
this pull request
Oct 28, 2026
…en to them in R-PAGE (#1574) Refs #1165 This is the second layer of the first #1165 stack, the keyset oracle the plan's question 27 settled on and the rule that reads it. The live GitLab record (`docs/development/gitlab-api-live.json`, written by `cmd/gen_api_live`) now says, for every route, where the code it reaches calls one of GitLab's keyset paginators and which parameter its next page is asked by, and it is re-recorded on 19.5.0 at schema version 5. R-PAGE reads it: it holds every action's `page_token` to the routes the action sends, and every list GitLab can page by keyset to the `next_page_token` it is followed by. Nothing in the record could give that answer before, because GitLab reads `pagination=keyset` without declaring it: of the 53 routes whose code reaches a keyset paginator at `v19.5.0-ee`, four declare `pagination` and 22 declare no cursor either. ## What the record says now Each route whose code reaches a keyset paginator carries a `keyset` list, one entry per place its code calls one. An entry names the paginator, the file and line of the call with the line quoted, the helpers the walk went through to reach it (`present_projects`, `render_work_items_collection_for`), the parameters the next page is asked by, and the feature flags the call is behind with their defaults. A route without `keyset` reaches no keyset paginator, so GitLab answers it with an offset page whatever `pagination` says; in schema 4 the same absence meant "not recorded", which is why the version moved, the same inversion every earlier bump guarded against. A site says where GitLab can serve a keyset page, not that it does. `paginate_with_strategies` picks `CursorPager` or `Keyset::Pager` by the model of the relation it is handed and answers 405 to a model and order it does not support, and the walk reads code, not the relation a request builds. So a `paginate_with_strategies` site carries `CursorPager`'s `cursor` unless the call passes `use_cursor: false`, which assumes a model with keyset orderings. The two service account token lists page `PersonalAccessToken`, which declares none: the record gives them `cursor`, and GitLab answers every keyset request there with 405 and never reads it. The comments of the field, of `apilive.KeysetSite` and of the script say so, and R-PAGE declares those routes rather than trusting the site (below). The five paginators the walk recognizes are `paginate_with_strategies`, `GitalyKeysetPager`, a finder executed with `gitaly_pagination: true` (the internal Orbit commit list), a route writing its own next link or cursor headers through `Gitlab::Pagination::Keyset::HeaderBuilder` (the instance audit events, the registry tags, the work item discussions), and `keyset_paginate` (the work item discussion notes and the MLflow searches). The record also carries `keyset_paginators`, which parameter each of GitLab's library paginators names in its next link. I did not copy those names into the script: it has `CursorPager`, `Keyset::Pager` and `GitalyKeysetPager` each write a next link through the method GitLab calls after serving a page, and reads the parameter back (`cursor`, `id_after` and `id_before`, `page_token`, and `cursor` for the `X-NEXT-CURSOR` header). A release that renames one changes the record rather than leaving it wrong. The source block counts the keyset routes, and the gate holds the count to the content like the other figures. ## How the walk works `introspect.rb` takes each route's block from the endpoint Grape built, compiles the helpers it is served with the way Grape does when it mounts the endpoint (a runner never mounts one), and reads the instruction sequence of the block and of every helper of GitLab's own it calls on the endpoint, following `super` into the CE method an EE module overrides. It records a site at a call to one of the paginators above; the cursor of `paginate_with_strategies` is `CursorPager`'s unless the call passes `use_cursor: false`, the cursor of a route's own next link is the key of the hash it hands the builder, and the cursor of `keyset_paginate` is the param it is handed. A feature flag is recorded on the sites it guards when it is tested as `if Feature.enabled?(:flag)` or `unless Feature.disabled?(:flag)`, which reads unambiguously from the branch instructions; that is how the commit list comes out behind `commits_keyset_pagination`, shipped off. The walk takes about forty seconds inside the container. The walk follows calls on the endpoint, never calls on a finder or a service, so a route a service pages by keyset on its own would read as paged by offset. That is what the new gate rule is for: a route declaring a parameter the record knows as a cursor is one that says it pages by keyset in its own words, and if it records no site, or no site asked by that parameter, the walk and GitLab disagree. All 31 routes that declare a cursor at 19.5.0 carry a site asked by it. ## What the gate refuses `make check-api-live`, and the generator before it writes, refuse a version 5 record without `keyset_paginators` (what a version 4 script labeled 5 produces), one below a floor of 35 keyset routes, a site naming a paginator nothing reads, a site with no file and line or no cursor, a probe naming no parameter or one nothing reads, and a route declaring a cursor the record does not follow. A passing run prints one more line, how many routes reach a keyset paginator and how many only behind a flag GitLab ships off. ## The re-record I booted `gitlab/gitlab-ee:19.5.0-ee.0` alone on the remote Docker host, the digest the record already names (`sha256:f04c71f565601283971d6472dd466609f4679450526f39aab3e3dcd6d23dc0ec`), with `go run ./cmd/gen_api_live/ -image gitlab/gitlab-ee:19.5.0-ee.0`. Apart from the keyset half, the note, the schema version and the provenance, the record is what it was: entities, routes, features and both fine-grained halves compare equal under `jq -S`, and the source figures are unchanged. The record marks 53 routes. Of the routes this server's actions send, it marks every one the first layer's classification found paging by keyset or refusing it (the 13 that follow it, the 5 refused by ordering, the 2 refused always and `project.list_user_contributed`). Each carries the cursor the classification found, `last` on the registry tags and `id_after`/`id_before` on the project lists included, except the two service account token lists, which the classification found refusing every keyset request and the record gives `CursorPager`'s `cursor`, for the reason above. Two of the routes it marks are ones the classification called ignored, and every other route it marks is one no action of ours sends: the work item REST routes, the MLflow searches, the internal Orbit routes, `GET /pipelines`, the package pipelines and the admin data management list. Of the two, the commit list (`repository.commit_list`, `repository.file_history`) reaches `GitalyKeysetPager` behind `commits_keyset_pagination`, which the record says is off by default, so it agrees with the classification on a default instance. `GET /groups` reaches `paginate_with_strategies` only for an anonymous caller (`present_groups_with_pagination_strategies` returns early for a signed-in one); the record names the helper but not that condition, since an early return is not a shape the walk reads, and this server always signs in. ## R-PAGE reads it R-PAGE asked whether a list says where it ends, and a keyset route raised no finding by design, because the params could not say which routes GitLab pages by keyset. Its new keyset half (`pagination.keyset` in the report, `cmd/audit_1to1/internal/paths/pagination_keyset.go`) joins each action to the REST routes it sends through `docs/development/action-requests.json`, so it judges at the action's own grain, and reports four problems: - `page-token-on-offset-route`: the action offers `page_token` and no route it sends can be paged by keyset, so GitLab ignores the parameter or refuses it. - `keyset-route-without-page-token`: a read whose output is a list sends a route GitLab can page by keyset and does not offer `page_token`. - `keyset-route-without-next-page-token`: the action offers `page_token` on a keyset route and its output carries no `next_page_token`. - `keyset-route-never-followed`: the action offers `page_token` on a keyset route and no package serving it (its owner, or the package of a handler or site the request record names) was recorded sending the route one of the parameters its next page is asked by. This one is at the inventory's package grain, which the half's `grain` says. A site behind a flag GitLab ships off counts as keyset: an instance that turns the flag on, GitLab.com for the commit list, pages the route by keyset, and the action's `page_token` is what reaches it there. A route the record marks and GitLab serves this server no keyset page it can follow is declared in a new category, `keyset-not-followable` (`declaredKeysetRoutes` in `pagination_declarations.go`), each entry with what GitLab's handler does at `v19.5.0-ee`, by file and line: - `GET /groups`: only an anonymous request reaches the keyset branch (`lib/api/groups.rb:171`). - `GET /groups/:id/service_accounts/:user_id/personal_access_tokens` and `GET /projects/:id/service_accounts/:user_id/personal_access_tokens`: `PersonalAccessToken` has no keyset orderings, so every keyset request is answered 405 (`lib/api/helpers/pagination_strategies.rb:30-43`). - `GET /users/:user_id/contributed_projects`: the finder is built without params, so the next link GitLab writes is never applied (upstream-bugs row 112). A declared route is held to as offset: an action offering `page_token` on one is reported, with the route in its `not_followable`, and a list sending one without `page_token` is declared. A declaration no finding uses is stale and fails the gate, like every other declaration table of R-PATH; the findings themselves report and do not gate, as the collection half's do. The route counts read the record's keyset field instead of the params: `keyset_routes` goes from the 10 routes that declare `per_page` without `page` to the 53 whose code reaches a keyset paginator, and a route can count as offset as well. Against the committed record, 202 actions offer `page_token`. 20 of them offer it on a keyset route, and every one publishes `next_page_token` and was followed in the unit suite. 182 offer it on an offset route, which is exactly the first layer's classification of ignored and keyset-not-followed actions without the commit list. The three lists that send a keyset route without `page_token`, `group.search` and the two token lists, are declared. Taking `page_token` out of those 182 inputs is the next stack of #1165. ## Also in this layer A second commit leaves `cmd/gen_api_live` with no mutant the mutation run cannot judge. `runnerDetail` cut the runner's stderr under a length test whose boundary is an equivalent mutant (at exactly the limit the cut and the whole are the same text), and the two boot defaults multiplied in package-level initializers, which no coverage profile records. The tail is now cut with `max` and the defaults are parsed from `"20m"` and `"10s"`; the values and the behavior are unchanged. ## How I tested it I wrote and measured this layer on the first layer as it stood over `e3e09d290`, and then rebased the stack twice: onto `main` at `6c9066aef`, after the OpenSSF Best Practices stack, and onto `main` at `56758fb71`, after the access level and edition tier stack (#1567 to #1571). The rebases changed nothing in this layer's lines. In the Go and Ruby files this layer touches, `main` changed only the copyright and SPDX header at their top; what it changed in the `Makefile`, `docs/development/cmd-utilities.md` and `docs/development/request-paths.md` lies outside the lines this layer edits there, and merged without a conflict. The six files this layer adds (`keyset.go` and its test in `cmd/gen_api_live` and in `cmd/internal/apilive`, `pagination_keyset.go` and its test in `cmd/audit_1to1/internal/paths`) now open with that header too, written by `make fix-file-headers` into the commit that adds each. What I ran again after the rebases is at the end of this section. The three generator tests the plan asked for came first and were red on the stack's first layer, `a35673d89` before the rebases, compiled against it unchanged: `TestRunGenerate_ARouteCarryingAKeysetPaginator_ComesOutMarked` (the route came out with `keyset = <nil>`, and so did the probe and the count), `TestRunCheck_ARouteDeclaringACursorTheRecordDoesNotFollow_IsRefused` and `TestRunCheck_AVersion4RecordLabeled5_IsRefused` (the parent refused both records only as `schema version 5 and this build reads version 4`). They edit the fixtures as JSON so they mean the same thing on a build with no keyset field, and they pass on this branch. Every rule has a case of its own in `TestKeysetProblems_EachRule_NamesWhatItRefuses`, with the whole sentence asserted, the floor at its edge and the shapes 19.5.0 records held to pass. The order of the generator's refusals has a case too: with the fine-grained and keyset refusals swapped, the second case of `TestRunGenerate_AKeysetSiteNothingReads_IsRefusedAfterTheFineGrainedHalf` fails. The R-PAGE cases read the keyset half as JSON, the way a reader of the report does, and were red on the parent commit, `1ece15937` before the rebases (`the pagination check carries no keyset half`, and the route counts read off the params), compiled against it unchanged; the cases of the declaration table are new code and could not compile there. `TestRun_TheRealTree_PassesItsOwnGate` now also holds the summary's keyset figures to the findings they count. `cmd/audit_1to1/internal/paths`, `cmd/gen_api_live`, `cmd/internal/apilive` and `cmd/gen_action_grants/internal/join` are at 100% statement coverage, gobco reads 1114/1114, 254/254, 186/186 and 304/304 conditions, and `make coverage-mutants` reads Killed 564, 216, 102 and 156, with Lived 0 and Timed out 0 on all four and Not covered 0 on the last three. `cmd/audit_1to1/internal/paths` also reports 483 mutants not covered, and every one is an `ARITHMETIC_BASE` on a `+` between string literals in a package-level initializer, on a line this layer does not change: the declaration tables of `sent_declarations.go`, `shape_declarations.go`, `endpoint_declarations.go`, `orbit_declarations.go` and `pagination_declarations.go`, a constant and a map of `published.go`, and a regular expression of `endpoints.go`. A `-` there does not compile, which is the tool artifact `docs/development/testing/README.md` describes; the new declaration table writes each reason as one literal for that reason, which took out the 14 it would have added. Before the rebase, `make analyze` passed every step but markdownlint, which could not start on the remote runner (`npx: not found`), and the same markdownlint command passed when run locally on the tree. `make check-api-live`, `make check-action-grants-derivation`, `make check-action-grants`, `make audit-1to1-grants`, `make audit-1to1-paths`, `make check-e2e-static`, `make check-tenancy`, `make check-test-file-names` and `go run ./cmd/format_md_tables/ --check` passed on the head, `node scripts/check-doc-links.mjs` passed, `GITLAB_MCP_TEST_SNAPSHOT_PARITY=deferred go test ./... -count=1` passed on the head (294 packages), and the e2e packages compiled. After the second rebase, on this head: `GITLAB_MCP_TEST_SNAPSHOT_PARITY=deferred go test ./... -count=1` passes (301 packages), `golangci-lint run --build-tags e2e` and `golangci-lint fmt --diff` pass on the four packages, the e2e packages compile, and `make check-api-live`, `make check-action-grants-derivation`, `make check-action-grants`, `make audit-1to1-grants`, `make audit-1to1-paths`, `make check-e2e-static`, `make check-tenancy`, `make check-test-file-names` and `make check-md-tables` pass. R-PAGE's figures are the ones above: 53 keyset routes, 202 actions offering `page_token`, 20 of them on keyset routes, 182 on offset routes and 3 lists declared. `make check-file-headers` and `make check-dco` against the first layer pass. I did not measure coverage, conditions or mutants again, since the rebases changed no statement of the four packages, and the SonarCloud commit below changes only the Ruby script, which no Go coverage profile reads. ## SonarCloud SonarCloud reported three findings on `cmd/gen_api_live/introspect.rb` in this pull request, and a fourth commit answers them without changing what the script records: - `rubydre:S7815`, a `raise` of a bare string in `next_link_of`: it now raises `MissingNextLinkError`, a `StandardError` of its own, with the same message. - `rubydre:S3776`, `KeysetWalk#scan` at a cognitive complexity of 25 against 15: `scan` keeps the loop over a sequence and hands each instruction to `scan_instruction`, which reads it in four steps of their own (`open_guard`, `read_operand`, `read_flag_call` and `follow_call`), and what it carried in locals from one instruction to the next is a `ScanState`. - `rubydre:S107`, `record_site` with ten parameters against seven: it takes the `ScanState`, the call, the index and the flags. The script only runs inside a booted GitLab, so I held the new walk to the old one directly: a harness loads the keyset half of both versions under one Ruby and walks a fixture whose methods reach every paginator kind the walk recognizes, flag guards of both recorded shapes and the shapes it leaves unrecorded (an early return, `if Feature.disabled?` and `unless Feature.enabled?`), nested blocks, helpers, a call to `super` and a recursive helper. Over five entry points both versions record the same 35 sites in the same order, with the same `via`, cursors and flags, and the probe fails with the same message. `go test ./cmd/gen_api_live/ ./cmd/internal/apilive/` and `make check-api-live` pass, and the record does not change, since it holds a digest of what the script answered and not of the script. On that commit SonarCloud then reported `rubydre:S131`, the `case` in `scan` with no default branch. A fifth commit gives it one that reads nothing, with a comment saying why: an instruction list holds line numbers, labels and events, and instructions. The harness finds the same 35 sites and the same error, and the two checks above pass again. ## What the top of this stack does The contributor documentation of schema 5 and of R-PAGE's keyset half (the `gen_api_live` section and the R-PAGE paragraph of `docs/development/cmd-utilities.md`, the `check-api-live` row of `docs/development/static-analysis.md`, the R-PAGE paragraph of `docs/development/request-paths.md` and its line in `CLAUDE.md`) and `docs/development/testing/testing.md` are refreshed at the top of this stack, with the rest of its regeneration. This layer regenerates no committed artifact, so `go run ./cmd/gen_testing_docs/ --check` fails here on the new tests, as the `FRESHNESS` deferral below a stack's top expects. ## #1517 Nothing here overlaps #1517, which changes neither the record, its generator nor R-PAGE, and no file of this layer is one #1517 changes. The merge request diffs route it pages is one the record marks as offset, as the review of #1517 expected, so R-PAGE reports `mr_review.changes_get` among the 182 actions offering `page_token` on an offset route; taking it out of that input is #1530, which waits for #1517. Whichever of #1517 and this stack lands second regenerates the artifacts both change. --------- Signed-off-by: José M. Requena Plens <mail@jmrp.io>
jmrplens
added a commit
that referenced
this pull request
Oct 28, 2026
…lists end to end, and regenerate the stack's artifacts (#1575) Refs #1165 This is the top of the first #1165 stack. It documents what the two layers below added, the keyset half of the live GitLab record at schema 5 and the keyset half of R-PAGE that reads it, adds the end-to-end scenario for the two service account token lists the first layer changed, and regenerates once every committed artifact the stack moves, which the layers below leave stale under the freshness deferral. ## The documentation The contributor pages described schema 4 and an R-PAGE that raises no keyset finding by design. In `docs/development/cmd-utilities.md`, the `gen_api_live` section now says what the walk records for each route (the paginator, the file and line quoted, the helpers it went through, the parameters the next page is asked by, the feature flags with their defaults), why a site says where GitLab can serve a keyset page rather than that it does, and what `-check` refuses of that half. The R-PAGE paragraph reads `keyset_routes` off the record's `keyset` field (53 routes, 39 of them offset too) rather than off the params (the 10 that declare `per_page` without `page`), and a new paragraph describes the keyset half: its four problems and their two grains, the `keyset-not-followable` declarations, and the figures against the committed record (202 actions offering `page_token`, 20 of them on keyset routes that publish `next_page_token` and were seen following it, 182 on routes GitLab pages by offset alone, three lists declared). `docs/development/request-paths.md` says the same in its R-PAGE bullet, `static-analysis.md` and the repository map in a line each, and the R-PAGE clause of `CLAUDE.md` names the rule and nothing more. On the site, the output format page, in English and Spanish, says that the two service account token lists, where GitLab refuses every keyset request, no longer take `pagination` or `page_token`, refuse either before sending anything and page with `page`, and that the lists still answering a keyset request with 405 are the ones given an order GitLab cannot page by keyset. ## The end-to-end scenario Nothing drove either token list against a real GitLab with a keyset argument, or with a sort order at all. Two scenarios in `test/e2e/gitlab/common/keyset_pagination_test.go`, `TestKeysetPagination_GroupServiceAccountTokens_PageByOffsetAlone` and `TestKeysetPagination_ProjectServiceAccountTokens_PageByOffsetAlone`, give a service account three tokens named so they sort in the order they were made, and on every surface: - send `pagination: "keyset"`, `page_token` and `order_by`, each held to a refusal made before any request: the SDK's on the individual surface, whose schema admits no property it does not list, execute's `Unknown params` on the dynamic surface, and the meta dispatcher's `unknown field`; - follow the list one token a page by page number in `name_asc` order, holding every page to carry no `next_page_token` and to name the next page while `has_more` holds; - read it whole in `name_desc` order, so an order GitLab ignored, which leaves the tokens unsorted, reads as a failure rather than as a listing. Both were red on the parent: driving the server built from `e3e09d290`, the base of the stack before it was rebased onto `main`, against CE 19.5.0, every surface failed at the first argument (8 tests, 8 failures), because that server still sends `pagination=keyset` and GitLab answers it, `405 {error: Keyset pagination is not yet available for this type of request}`: `refused as tool_error, and the test expected invalid_params` on the individual surface and `the error does not mention "unknown field \"pagination\""` on the other two. That run was of the scenarios' first version, whose dynamic case expected the meta wording; the complete CE run of this stack showed that execute refuses the argument earlier, with `Unknown params: pagination.` and the list of valid params, and the dynamic case now expects that. With the final version, the red run on the parent fails again on every surface: GitLab answers `405 {error: Keyset pagination is not yet available for this type of request}` for both `group.service_account_pat_list` and `project.service_account_pat_list`. On the stack's head before the rebases, the green run of the two scenarios passes. ## The regeneration One commit, with the comparison checked: - the tool snapshots of the individual surface and of the compact and full meta schemas, which change on exactly the two token list tools and the `gitlab_group` and `gitlab_project` schemas that carry them (`pagination`, `page_token` and `order_by` gone, `sort` naming the eight documented orders); the curated `tools_meta.json` does not change; - the per-domain tool reference of the group and project groups, in English and Spanish, the three llms files, the token footprint with the README block it writes, and the testing reference, refreshed with `-skip-coverage`; - the request inventory, recorded again on the whole unit suite and merged: the two token routes are no longer recorded sending `order_by`, `page_token` or `pagination`, and the group one is now recorded sending `page` and `per_page`. The action request record and the fine-grained table, the site stats, the brand assets, the manifests and the e2e coverage page regenerate unchanged. ## How I tested it The stack was written on `e3e09d290` and rebased twice: onto `main` at `6c9066aef`, after the OpenSSF Best Practices stack, and onto `main` at `56758fb71`, after the access level and edition tier stack (#1567 to #1571). In this layer's two hand-written commits the first rebase changed one place: `main` added rows to the pipe table of `docs/development/static-analysis.md` and padded it again, so I took `main`'s table, put this layer's sentence back into the `check-api-live` row and ran `go run ./cmd/format_md_tables/`. The second changed nothing in them: what `main` changed in `CLAUDE.md`, `docs/development/cmd-utilities.md` and `docs/development/repository-map.md` lies outside the lines this layer edits there, and merged without a conflict. Both times the regeneration commit was not carried over: I dropped it and regenerated every artifact on the rebased tree. It changes the same 15 files as before. The tool snapshots, the tool reference, the llms files and the request inventory, recorded again on the whole rebased unit suite, change line for line as they did on the old base. The token footprint with its README block and the testing reference move by the same amounts from `main`'s new figures: 225 fewer tokens on each tier's individual surface, and 37 more test functions and three more test files under `cmd/`, the stack's own tests. With the freshness checks on, on this head after the second rebase: `go test ./... -count=1` passes (301 packages), and again with `GITLAB_MCP_TEST_SNAPSHOT_PARITY=deferred`. `make analyze` passes 30 of its 32 steps on the remote runner, and the other two pass locally on the same tree: markdownlint (step 6, which needs Node) and the copyright and license headers (step 32, which reads git's file list). Every check the "Generated artifacts" job of CI runs passes, among them `check-tool-reference`, `check-llms`, `check-footprint`, `check-site-stats`, `check-testing-docs`, `check-request-inventory`, `check-api-live`, `audit-1to1-paths`, `check-action-grants`, `check-e2e-static`, `check-tenancy` and `check-file-headers`, with `check-openplugin` and `check-server-json-packages` run locally since they need the network. `make check-dco` passes for the ten commits of the stack (the tenth is the second layer's answer to SonarCloud's `rubydre:S131`, which changes no recorded output). `make test-e2e-http` (780 tests, 8 skipped because the runner has no Docker) and `make test-e2e-stdio` (277 tests) pass, the e2e packages compile, and `test/e2e/gitlab/common` passes `golangci-lint run` and `golangci-lint fmt --diff`. The site passes `i18n:check`, `facts:check`, `chips:check`, `llms:check`, `build` and `lint`, and `node scripts/check-doc-links.mjs` passes. Against Docker GitLab 19.5.0, the complete CE run (`make test-e2e-ce` with the skip gate) ran on this stack before the dynamic case of the two new scenarios was corrected: 1313 tests, and the only failures were those two dynamic cases, for the wording above. Its skip gate also failed on four skips, the Bitbucket Cloud and GitHub imports, because that worktree had no `.env` with their credentials, which the next run has. The correction touches only that test file. On the stack's head before the rebases (`25b20c981`), with the skip gate on, the complete CE run passes 1313 tests (1 skip, which is declared) and the complete EE run passes 1642 tests (no skips). I did not run the Docker suites again after the rebases. Since that run, the server code this stack changes is the same apart from the comment that names the register row (now 143) and the file headers, and the Ruby script the second layer's SonarCloud commit splits, which only `gen_api_live` runs, inside a GitLab it boots. What `main` brought that those suites exercise is the dependency update of #1564 (`golang.org/x/net` 0.62.0, `google.golang.org/grpc` 1.85.0, `gqlparser` 2.5.64 and `tiktoken-go` 0.10.0 among them, client-go and the go-sdk unchanged), the Bitbucket Server fixture moved from 9.6 to 10.5 with basic authentication forced back on, which the import scenarios use, the copyright and SPDX header in every source file, which is comments, and the access level and edition tier stack: the access levels each write offers, cut to what its route accepts, a pending access request no longer reported as approved, the two checksum filters gone from the project lists and `new_epic` from a project's notification settings, and scenarios of its own in `accessrequests_test.go` and `accesstokens_test.go`. The plaintext listener warning of #1561 is HTTP mode only, and the e2e suites drive stdio. None of it touches the two token lists, their packages or the scenarios this stack adds. ## #1517 #1517 is open, and the maintainer has asked for changes before merging it. This stack leaves to it everything it does or was asked to do: the offset paging of the merge request diffs in `mr_review.changes_get`, with the pagination block its output gains and without keyset continuation, the page walk of the inline comment pre-check under RQB-013 with its check of the position's SHAs, and the curated text, request declarations and test moves the review asked for. R-PAGE classifies `mr_review.changes_get` with the rest, among the 182 actions offering `page_token` on an offset route, no declaration of it names that action, and nothing here changes it. Its unused `order_by`, `sort`, `pagination` and `page_token` are #1530 and the pre-check's `position_type` is #1531, both after #1517. The two share no hand-written file: this stack changes `cmd/audit_1to1/internal/structs/analyze.go`, the `groupserviceaccounts` and `projectserviceaccounts` packages, R-PAGE in `cmd/audit_1to1/internal/paths` (`analyze.go`, `pagination*.go`, `shapes.go`) and the live record's generator, and #1517 changes `sent_declarations.go` in the same package, the `mr*` packages, `internal/toolutil/diff_position.go` and `internal/tenancy`, so a rebase of either is mechanical. Both change generated files: `internal/tools/testdata/tools_individual.json`, `tools_meta_compact.json` and `tools_meta_full.json`, `docs/development/request-inventory.json`, `docs/development/token-footprint.md` and `site/src/data/token-footprint.json` with the README token block, `docs/development/testing/testing.md`, `llms-full.txt` and `llms-full-individual-tools.txt`. Whichever of #1517 and this stack lands second regenerates those files on top of the other, together with the ones only #1517 changes and this stack regenerates unchanged (`action-requests.json`, the fine-grained table, `tools_meta.json`, `llms-full-meta-tools.txt` and the `mr-review` tool reference). --------- Signed-off-by: José M. Requena Plens <mail@jmrp.io>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Refs #1280
Refs #1240
This is the top of the access level and edition tier stack. The four layers below it, the access levels of #1280 and the three layers that answer #1240, regenerate nothing under the freshness deferral, and this layer regenerates once every committed artifact they move. It adds no behavior of its own: one commit, the regeneration and the R-PATH declaration that can only land with it.
The stack sits on
mainat6c9066aef, onto which I rebased it on 28 October 2026, layer by layer and in order. Every commit is signed and carries aSigned-off-bytrailer. The five files the layers add carry the copyright and license header of #1563, written bymake fix-file-headersinto the commit that adds each one.maintook row 140 of the upstream register for gobco, so the two rows the layers add are 141 and 142. The regeneration was made again on the rebased tree rather than rebased.The regeneration
The layers changed the surface every generated artifact describes. The access level parameters publish the levels each route accepts as integer enums, the group access request approval reads the group and the requester for a Minimal access approval, and the group, member, LDAP link, group variable, issue, merge request, notification and project parameters are tier tagged or described by tier.
gitlab_access,gitlab_group,gitlab_member_roleandgitlab_projecton the opaque meta surface;gitlab_admin,gitlab_ci_variable,gitlab_issueandgitlab_useras well on the full one).-skip-coverage.access.approve_group(GET /groups/:idandGET /groups/:id/members/:user_id), and the fine-grained permissions reference that prints them.new_epicflag a group notification settings update now sends, and nothing else moves.The site stats, the brand assets, the manifests, the model evaluation pages, the charts and the e2e coverage page regenerate unchanged, and a second run of every generator over the committed result changes nothing.
The R-PATH declaration
R-PATH reads the group the approval fetches as a
GroupDetailthe package never publishes. I declared it beside the LDAP link listing's existence probe, as a read the package makes for its own use: it is sent only whenaccess.approve_groupis asked foraccess_level5 on a licensed session, readsparent_idalone to refuse the approval on a subgroup before it is sent, and returns nothing of the answer. The declaration lands here because only this commit's inventory holds the read, and a declaration that answers nothing fails the audit.How I tested it
With the freshness checks on, on this head:
go test ./... -count=1passes (301 packages),make analyzepasses every step (markdownlint, which needs Node, and the file header check, which reads git's file list, run locally on the same tree), every check the "Generated artifacts" job of CI runs passes, among themcheck-tool-reference,check-llms,check-footprint,check-site-stats,check-testing-docs,check-request-inventory,check-file-headers,audit-1to1-paths,check-action-grants,check-action-grants-derivation,audit-1to1-grants,check-e2e-staticandcheck-tenancy, as docheck-server-json,check-server-json-packagesandcheck-openplugin,make check-dcopasses on every commit of the stack,make test-e2e-http(780 tests, 8 of them skipped for want of Docker on the runner) andmake test-e2e-stdio(277 tests) pass, and the e2e packages compile. The site passesi18n:check,facts:check,chips:check,llms:check,buildandlint, andnode scripts/check-doc-links.mjspasses.Against Docker GitLab 19.5.0 both complete runs passed with the skip gate on, on this stack as it stood on
e3e09d290before the rebase:make test-e2e-ceran 1321 tests with no failure and one skip, the declared opt-in throttle probe;make test-e2e-ee, licensed, ran 1650 tests with no failure and no skip. The scenarios the layers added passed on all three surfaces: on CE, the Minimal access approval refused on the Free tier, a level the route does not grant refused with the request still pending, an Owner-level project access token granted to an Owner's token, and an issue created for two assignees keeping only the first; on EE, the same refusal and token, the Minimal access approval refused on a subgroup and granted on a licensed top-level group, and an instance custom role on an Owner base refused. I did not run them again after the rebase. Whatmainbrought in between does not reach those scenarios' requests: the header lines are comments, the plain HTTP listener check of #1561 is read only by an HTTP listener while the suite drives the binary over stdio, the Orbit fixtures moved for the GitLab.com suite, the Bitbucket fixture's image moved to 10.5 for the import coverage behind its own profile, and of the dependency updates of #1564 the binary linksgolang.org/x/net, whose IDNA packageinternal/gitlabuses, and the gRPC modules of the OTLP exporter, which runs only with telemetry turned on; the unit suite and both transport suites above run on them.#1517
#1517 is open, and the maintainer has asked for changes before merging it. This stack leaves to it everything it does or was asked to do: the offset paging of the merge request diffs in
mr_review.changes_get, the page walk of the inline comment pre-check under RQB-013 with its check of the position's SHAs against the merge request'sdiff_refs, and the curated text, request declarations and test moves the review asked for; nothing here touchesmrchanges,mrdiscussions,mrdraftnotes,internal/toolutil/diff_position.goor the tenancy register. Its unusedorder_by,sort,paginationandpage_tokenare #1530 and the pre-check'sposition_typeis #1531, both after #1517. Two hand-written files are changed by both, in different blocks, so a rebase of either is mechanical:cmd/audit_1to1/internal/paths/sent_declarations.go, where this stack adds the access request probe to the existence probes and its reason after the LDAP one while #1517 adds the merge request rows to the position pre-check block and their reason, and theinternal/tools/testdata/tools_meta.jsongolden, where this stack changes thegitlab_access,gitlab_group,gitlab_member_roleandgitlab_projectentries and #1517 thegitlab_mr_reviewone. Both change generated files:tools_individual.json,tools_meta_compact.jsonandtools_meta_full.json,docs/development/request-inventory.json,docs/development/action-requests.json,internal/tools/actiongrants/table_gen.go,docs/development/token-footprint.mdandsite/src/data/token-footprint.jsonwith the README token block,docs/development/testing/testing.md,llms-full.txt,llms-full-individual-tools.txtandllms-full-meta-tools.txt. Whichever of #1517 and this stack lands second regenerates those files on top of the other, together with themr-reviewtool reference, which only #1517 changes and this stack regenerates unchanged.